﻿using System;
using System.Collections.Generic;

namespace LinkDownloader.Core
{
    public class Downloader
    {

        public delegate void ChangedEventHandler(int id, ChangedEventArgs e);
        public delegate void ProgressChangedEventHandler(int id, int percent);
        public delegate void AllChangedEventHandler(string message);

        public event ChangedEventHandler JobAdded;
        public event ChangedEventHandler JobStarted;
        public event ChangedEventHandler JobCancelled;
        public event ChangedEventHandler JobSkipped;
        public event ChangedEventHandler LinkError;
        public event ChangedEventHandler JobDownloadError;
        public event ChangedEventHandler JobCompleted;
        public event AllChangedEventHandler AllJobsCompleted;
        public event ProgressChangedEventHandler JobProgressChanged;

        private string username = string.Empty;
        public string Username
        {
            get { return this.username; }
            set { this.username = value; }
        }

        private string password = string.Empty;
        public string Password
        {
            get { return this.password; }
            set { this.password = value; }
        }
        
        private string fileDirectory = string.Empty;
        public string SaveDirectory
        {
            get { return this.fileDirectory; }
            set { this.fileDirectory = value; }
        }

        public int AllJobCount
        {
            get { return (this.ActiveJobCount + this.OnlineJobCount + this.CancelledJobCount + this.CompletedJobCount); }
        }

        private List<DownloadJob> activeJobs = new List<DownloadJob>();
        public List<DownloadJob> ActiveJobs
        {
            get { return this.activeJobs; }
            set { this.activeJobs = value; }
        }
        
        public int ActiveJobCount
        {
            get { return this.activeJobs.Count; }
        }

        private List<DownloadJob> onlineJobs = new List<DownloadJob>();
        public List<DownloadJob> OnlineJobs
        {
            get { return this.onlineJobs; }
            set { this.onlineJobs = value; }
        }

        public int OnlineJobCount
        {
            get { return this.onlineJobs.Count; }
        }

        private List<DownloadJob> cancelledJobs = new List<DownloadJob>();
        public List<DownloadJob> CancelledJobs
        {
            get { return this.cancelledJobs; }
            set { this.cancelledJobs = value; }
        }

        public int CancelledJobCount
        {
            get { return this.cancelledJobs.Count; }
        }

        private List<DownloadJob> completedJobs = new List<DownloadJob>();
        public List<DownloadJob> CompletedJobs
        {
            get { return this.completedJobs; }
            set { this.completedJobs = value; }
        }

        public int CompletedJobCount
        {
            get { return this.completedJobs.Count; }
        }

        private bool skipOnError = false;
        public bool SkipOnError
        {
            get { return this.skipOnError; }
            set { this.skipOnError = value; }
        }

        private int globalCounter = 0;
        private Dictionary<int, int> indices = new Dictionary<int, int>();
        private Dictionary<string, DownloadJob> links = new Dictionary<string, DownloadJob>();
        
        public Downloader()
        {
            // do nothing
        }

        #region "Overridable Methods"

        // Invoke the Changed event; called whenever list changes:
        protected virtual void OnJobAdded(int id, ChangedEventArgs e)
        {
            if (this.JobAdded != null) { this.JobAdded(id, e); }
        }

        public virtual void OnJobStarted(int id, ChangedEventArgs e)
        {
            this.SetJobStarted(id);
            if (this.JobStarted != null) { this.JobStarted(id, e); }            
        }

        public virtual void OnJobCancelled(int id, ChangedEventArgs e)
        {
            
            int index = this.GetJobByID(id);
            if (index >= 0) { this.SetJobCancelled(index); }

            if (this.ActiveJobCount <= 0)
            {
                this.OnAllJobsCompleted("All downloads are done!");
            }

            if (this.JobCancelled != null) { this.JobCancelled(id, e); }
            
        }

        public virtual void OnJobDownloadError(int id, ChangedEventArgs e)
        {
            if (this.JobDownloadError != null) { this.JobDownloadError(id, e); }
        }

        protected virtual void OnLinkError(int id, ChangedEventArgs e)
        {
            if (this.LinkError != null) { this.LinkError(id, e); }
        }

        public virtual void OnJobCompleted(int id, ChangedEventArgs e)
        {

            int index = this.GetJobByID(id);
            if (index >= 0) { this.SetJobCompleted(index); }

            if (this.ActiveJobCount <= 0)
            {
                this.OnAllJobsCompleted("All downloads are done!");
            }

            if (this.JobCompleted != null) { this.JobCompleted(id, e); }
            
        }

        public virtual void OnJobSkipped(int id, ChangedEventArgs e)
        {
            if (this.JobSkipped != null) { this.JobSkipped(id, e); }
        }

        public virtual void OnJobProgressChanged(int id, int percent)
        {
            if (this.JobProgressChanged != null) { this.JobProgressChanged(id, percent); }
        }

        protected virtual void OnAllJobsCompleted(string message)
        {
            if (this.AllJobsCompleted != null) { this.AllJobsCompleted(message); }
        }

        protected virtual DownloadJob GetNewDownloadJob(int id)
        {
            return new DownloadJob(this, id);
        }

        #endregion

        private DownloadJob GetNewDownloadJob()
        {
            DownloadJob d = this.GetNewDownloadJob(this.globalCounter);
            if (d == null) { d = new DownloadJob(this, this.globalCounter); }
            this.globalCounter++;
            return d;
        }

        public void StartDownload(string link)
        {
            int i = this.AddNewDownload(link);
            if (i >= 0) { this.StartDownload(i); }
        }

        public void StartDownload(int index)
        {
            DownloadJob job = this.onlineJobs[index];
            job.StartDownload();
            this.ActivateJob(job, index);
        }

        public void StartAll()
        {
            for (int i = this.onlineJobs.Count - 1; i >= 0; i--)
            {
                this.StartDownload(i);
            }
        }

        public DownloadJob[] AddNewDownloads(string[] links)
        {
            List<DownloadJob> added = new List<DownloadJob>(links.Length);
            for (int i = 0; i < links.Length; i++)
            {
                int ret = this.AddNewDownloadToList(links[i], i);
                if (ret >= 0) 
                {
                    added.Add(this.onlineJobs[ret]);
                }
            }
            return added.ToArray();
        }

        public int AddNewDownload(string link)
        {
            return this.AddNewDownloadToList(link, -1);
        }

        private int AddNewDownloadToList(string link, int index)
        {

            try
            {
                Uri uri = new Uri(link);
            }
            catch (Exception exception)
            {
                ChangedEventArgs e = new ChangedEventArgs();
                e.Message = link + Environment.NewLine + exception.Message;
                this.OnLinkError(index, e);
                return -1;
            }
                        
            DownloadJob download = this.GetNewDownloadJob();
            download.FileDirectory = this.fileDirectory;
            download.Link = link;
            download.Password = this.password;
            download.Username = this.username;
            return this.AddNewIdlingJob(download);

        }

        public void CancelDownload(int index)
        {

            DownloadJob job = this.activeJobs[index];
            try
            {
                job.Worker.CancelAsync();
                ChangedEventArgs e = new ChangedEventArgs();
                e.Message = "Cancelled.";
                this.OnJobCancelled(job.ID, e);
            }
            catch (Exception exception)
            {
                ChangedEventArgs e = new ChangedEventArgs();
                e.Message = exception.Message;
                this.OnJobDownloadError(job.ID, e);
            }

            this.SetJobCancelled(index);

        }

        public void CancelAll()
        {
            for (int i = this.activeJobs.Count - 1; i >= 0; i--)
            {
                this.CancelDownload(i);
            }
        }

        protected int AddNewIdlingJob(DownloadJob job)
        {
            this.onlineJobs.Add(job);
            this.OnJobAdded(job.ID, new ChangedEventArgs() { Argument = job, Message = "New download added to queue.", Success = true });
            return this.onlineJobs.Count - 1;
        }

        protected void ActivateJob(DownloadJob job, int idlingIndex)
        {
            this.onlineJobs.RemoveAt(idlingIndex);
            this.activeJobs.Add(job);
            this.indices.Add(job.ID, this.activeJobs.Count - 1);
        }

        protected void SetJobStarted(int index)
        {
            DownloadJob job = this.onlineJobs[index];
            this.ActivateJob(job, index);
        }

        /// <summary>
        /// Classifies the specified job as cancelled.
        /// </summary>
        /// <param name="index">Index of the job in the active list. Use the method 'GetJobByID' to get the job index.</param>
        /// <param name="addToStart">Specifies if the job needs to be added to the start of the cancelled list. Set to false by default.</param>
        protected void SetJobCancelled(int index, bool addToStart = false)
        {
            DownloadJob job = this.activeJobs[index];
            this.activeJobs.RemoveAt(index);
            if (addToStart)
            {
                this.cancelledJobs.Insert(0, job);
            }
            else
            {
                this.cancelledJobs.Add(job);
            }
            this.indices.Remove(job.ID);
        }

        /// <summary>
        /// Classifies the specified job as completed.
        /// </summary>
        /// <param name="index">Index of the job in the active list. Use the method 'GetJobByID' to get the job index.</param>
        /// <param name="addToStart">Specifies if the job needs to be added to the start of the completed list. Set to false by default.</param>
        protected void SetJobCompleted(int index, bool addToStart = false)
        {
            DownloadJob job = this.activeJobs[index];
            this.activeJobs.RemoveAt(index);
            if (addToStart)
            {
                this.completedJobs.Insert(0, job);
            }
            else
            {
                this.completedJobs.Add(job);
            }
            this.indices.Remove(job.ID);
        }

        /// <summary>
        /// Gets the job index of the specified job in the active list.
        /// </summary>
        /// <param name="id">Job ID of the asssociated job.</param>
        /// <returns>The job index if in the active list, else returns -1 if not found in the active list.</returns>
        protected int GetJobByID(int id)
        {
            int i;
            if (this.indices.TryGetValue(id, out i))
            {
                return i;
            }
            return -1;
        }
        
    }
          
}
